﻿using System;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;
using Microsoft.Scripting.Runtime;
using System.Windows;
using Microsoft.JScript.Runtime.Types;
using Microsoft.Popfly.GameCreator.GameEngine.Events;
using Microsoft.Popfly.GameCreator.GameEngine;
using Microsoft.Popfly.GameCreator.GameEngine.Behaviors.JSRuntime;
using System.Runtime.CompilerServices;
using Microsoft.Popfly.GameCreator.GameEngine.Behaviors;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Windows.Threading;
using Microsoft.JScript.Runtime.GameCreatorStuff;
using System.Windows.Browser;


[assembly: ExtensionType(typeof(BehaviorOwner), typeof(JSRuntimeExtensions))]
[assembly: ExtensionType(typeof(FrameworkElement), typeof(JSRuntimeUIExtensions))]
[assembly: ExtensionType(typeof(DependencyObject), typeof(JSRuntimeDependentExtensions))]
[assembly: ExtensionType(typeof(System.Windows.Controls.TextBlock), typeof(JSRuntimeTextBlockExtensions))]
namespace Microsoft.Popfly.GameCreator.GameEngine.Behaviors.JSRuntime
{
    public static class JSCriptObjectExtensions
    {
        public static T GetAsType<T>(this JSObject obj, String key, T def, Func<Object, Object> parserFunction)
        {
            try
            {
                Object refObj = obj[key];
                if (refObj != null && !(refObj is JScript.Runtime.Types.UnDefined))
                {
                    return (T) parserFunction(refObj);
                }
                return def;
            }
            catch (Exception)
            {
                return def;
            }
        }
        public static T GetAsType<T>(this JSObject obj, String key, T def)
        {
            return GetAsType<T>(obj, key, def,
                refObj => {
                    var TType = typeof(T);

                    // Since concatstrings can be assigned to strings but not cast as strings, set refobj to the string
                    // version of refobj if it's a concatstring
                    if (refObj.GetType().Equals(typeof(Microsoft.JScript.Runtime.Types.ConcatString)))
                    {
                        string refObjString = (Microsoft.JScript.Runtime.Types.ConcatString)refObj;
                        refObj = refObjString;
                    }
                    if (TType.IsEnum)
                    {
                        string refObjString = (string) refObj;
                        if (refObjString == "")
                        {
                            return def;
                        }
                        else
                        {
                            return (T)Enum.Parse(TType, (String)refObj, true);
                        }
                    }
                    else if (TType.Equals(typeof(int)) && refObj is string)
                    {
                        string refObjString = (string) refObj;
                        if (refObjString == "")
                        {
                            return def;
                        }
                        else
                        {
                            return int.Parse((string)refObj);
                        }
                    }
                    else if (TType.Equals(typeof(double)) && refObj is string)
                    {
                        string refObjString = (string) refObj;
                        if (refObjString == "")
                        {
                            return def;
                        }
                        else
                        {
                            return double.Parse((string)refObj);
                        }
                    }
                    else if (TType.Equals(typeof(bool)) && refObj is string)
                    {
                        string refObjString = (string) refObj;
                        if (refObjString == "")
                        {
                            return def;
                        }
                        else
                        {
                            return bool.Parse((string)refObj);
                        }
                    }
                    if (refObj is JSObject && ((JSObject)refObj).Count == 0)
                    {
                        return def;
                    }
                    return (T) refObj;
                });
        }
    }

    public static class JScriptRuntimeHolder
    {
        public static ScriptRuntime rt { get; private set; }
        public static ScriptEngine jsEng { get; private set; }
        static JScriptRuntimeHolder()
        {
            rt = Microsoft.JScript.Runtime.JScript.CreateRuntime();
            rt.LoadAssembly(typeof(JSRuntimeExtensions).Assembly);
            rt.LoadAssembly(typeof(Microsoft.JScript.Runtime.GameCreatorStuff.Vector2).Assembly);
            jsEng = rt.GetEngine("jscript");
        }
    }
    public static class JSRuntimeUIExtensions
    {
        public static object findName(FrameworkElement element, String name)
        {
            return element.FindName(name);
        }
        private static DependencyProperty GetDependentProperty(string name)
        {
            if (name.Equals("Canvas.Top", StringComparison.InvariantCultureIgnoreCase))
            {
                return System.Windows.Controls.Canvas.TopProperty;
            }
            else if (name.Equals("Canvas.Left", StringComparison.InvariantCultureIgnoreCase))
            {
                return System.Windows.Controls.Canvas.LeftProperty;
            }
            else if (name.Equals("Canvas.ZIndex", StringComparison.InvariantCultureIgnoreCase))
            {
                return System.Windows.Controls.Canvas.ZIndexProperty;
            }
            return null;
        }
        [SpecialName]
        public static void SetItem(FrameworkElement Obj, string name, object value)
        {
            DependencyProperty props = GetDependentProperty(name);
            if (props != null)
            {
                Obj.SetValue(props, value);
            }
        }

        [SpecialName]
        public static Object GetItem(FrameworkElement Obj, string name)
        {
            DependencyProperty props = GetDependentProperty(name);
            if (props != null)
            {
                return Obj.GetValue(props);
            }
            return null;
        }


    }
    public static class JSRuntimeTextBlockExtensions
    {
        [SpecialName]
        public static void SetMemberAfter(System.Windows.Controls.TextBlock Obj, string name, object value)
        {
            if (name.Equals("UnicodeString", StringComparison.InvariantCultureIgnoreCase))
            {
                if (value is Microsoft.JScript.Runtime.Types.ConcatString)
                {
                    Microsoft.JScript.Runtime.Types.ConcatString concatString = (Microsoft.JScript.Runtime.Types.ConcatString)value;
                    Obj.Text = concatString;
                }
                else if (value is String)
                {
                    Obj.Text = (String)value;
                }
                else
                {
                    Obj.Text = value.ToString();
                }
            }
        }

        [SpecialName]
        public static Object GetBoundMember(System.Windows.Controls.TextBlock Obj, string name)
        {
            if (name.Equals("UnicodeString", StringComparison.InvariantCultureIgnoreCase))
            {
                return Obj.Text;
            }
            return null;
        }
    }

    public static class JSRuntimeDependentExtensions
    {
        private delegate void eventRemover();
        private static Dictionary<int, Delegate> removeElement = new Dictionary<int, Delegate>();
        public static int AddEventListener(DependencyObject element, String eventName, FunctionObject invokeFunction)
        {
            System.Reflection.EventInfo evt = element.GetType().GetEvent(eventName);
            System.EventHandler evtHandler = delegate(object sender, EventArgs e)
            {
                invokeFunction.Call(null, element, new object[] { sender, e });
            };
            eventRemover evtRemover = delegate()
            {
                evt.RemoveEventHandler(element, evtHandler);
            };

            evt.AddEventHandler(element, evtHandler);
            int hashCode = element.GetHashCode() ^ eventName.GetHashCode() ^ invokeFunction.GetHashCode();
            removeElement.Add(hashCode, evtRemover);
            return hashCode;
        }
        public static void RemoveEventListener(FrameworkElement element, String eventName, int token)
        {
            System.Delegate evtRemover;
            if (removeElement.TryGetValue(token, out evtRemover))
            {
                removeElement.Remove(token);
                evtRemover.DynamicInvoke();
            }
        }

    }
    public static class JSRuntimeExtensions
    {
        internal class InvokedFunction : IBehavior
        {
            public FunctionObject Function { get { return m_funObj; } }
            private FunctionObject m_funObj;
            private BehaviorOwner m_owner;
            public InvokedFunction(BehaviorOwner owner, FunctionObject funObj)
            {
                this.m_owner = owner;
                this.m_funObj = funObj;
            }

            #region IBehavior Members

            public void Invoke(BehaviorOwner sender, PGCEventArgs args)
            {
                try
                {
                    m_funObj.Call(null, m_owner, new Object[] { sender, args });
                }
                catch (Exception e)
                {
                    string sceneOrActor = "scene";
                    if (m_owner.IsActor) sceneOrActor = "actor";

                    Console.ReportError(
                        "Error executing custom code spawned behavior on " + 
                        sceneOrActor + " '" + m_owner.Name + "'.\n" +
                        "Message: '" + e.Message + "'\n" + "Exception: " + e.ToString());
                }
            }

            #endregion
        }
    }
    public class Window
    {
        public static void open(String url)
        {
            HtmlPage.Window.Eval("window.open('" + url + "');");
        }
    }
    public class Math : JScript.Runtime.Types.MathObject
    {

        private Math()
            : base(null, null)
        {
        }
        public static double rand()
        {
            return random();
        }
        public static double Sqrt(double d)
        {
            return System.Math.Sqrt(d);
        }
        public static double degreesToRadians(double degrees)
        {
            return degrees * System.Math.PI / 180;
        }
        public static double radiansToDegrees(double radians)
        {
            return radians * 180 / System.Math.PI;
        }

        public static Vector2 getRotatedOffset(Point p, double angle)
        {
            return getRotatedOffset(new Vector2(p), angle);
        }
        public static Vector2 getRotatedOffset(Vector2 pt, double angle)
        {
            if (pt.X == 0 && pt.Y == 0)
                return pt;

            // Convert to radians
            var curAngle = degreesToRadians(angle);

            // Express the Data.Vector2 in polar coordinates (r,theta)
            var u = pt.X;
            var v = pt.Y;
            var r = System.Math.Sqrt(System.Math.Pow(u, 2) + System.Math.Pow(v, 2));
            var theta = System.Math.Asin(v / r);

            // Rotate to (r, theta+angle)
            var newU = r * System.Math.Cos(theta + curAngle);
            var newV = r * System.Math.Sin(theta + curAngle);

            return new Vector2()
            {
                X = newU,
                Y = newV
            };
        }
        public static Vector2 vector2Add(double x1, double y1, double x2, double y2)
        {
            return new Vector2() { X = x1 + x2, Y = y1 + y2 };
        }
        public static Double vector2Dot(double x1, double y1, double x2, double y2)
        {
            return (x1 * x2) + (y1 * y2);
        }

    }
    public static class JSUtils
    {
        public static void SetTimeout(JScript.Runtime.Types.FunctionObject func, int timeout)
        {
            var timer = new DispatcherTimer();
            timer.Interval = new TimeSpan(timeout * 10000);
            timer.Tick += delegate(object sender, EventArgs e)
            {
                timer.Stop();
                func.Call(null, new Object[0]);
            };
            timer.Start();
        }
    }
}
namespace Microsoft.Popfly.GameCreator.GameEngine.Behaviors
{
    public class CustomBehavior : IBehavior
    {
        private ScriptScope scope;
        private CompiledCode compiledCode;
        private string Code;
        private Game m_game;
        private Object owner;

        public CustomBehavior(Object owner, Game game, string code)
        {
            this.owner = owner;
            m_game = game;
            Code = code;

            scope = JScriptRuntimeHolder.jsEng.CreateScope();
            scope.SetVariable("Game", game);
            ScriptSource source;

            ScriptSource globalHost = JScriptRuntimeHolder.jsEng.CreateScriptSourceFromString(@"
JSUtils = ImportAlias('Microsoft.Popfly.GameCreator.GameEngine.Behaviors.JSRuntime.JSUtils');
console = ImportAlias('Microsoft.Popfly.GameCreator.GameEngine.Console');
Math = ImportAlias('Microsoft.Popfly.GameCreator.GameEngine.Behaviors.JSRuntime.Math');
Vector2 = ImportAlias('Microsoft.JScript.Runtime.GameCreatorStuff.Vector2');
window = ImportAlias('Microsoft.Popfly.GameCreator.GameEngine.Behaviors.JSRuntime.Window');

function setTimeout(func, timeout) 
{ 
    return JSUtils.SetTimeout(func, timeout); 
}

function delegate(target, callback) 
{
    return function(){
        if(callback.apply != null)
            return callback.apply(target, arguments); 
        else
            return callback();
    };
}

Game.CreateMotionBehavior = function(params)
{ 
    return function(){ return Game._CreateMotionBehavior(this, params); };
};
",
                SourceCodeKind.Statements);
            CompiledCode globalCode = globalHost.Compile();
            globalCode.Execute(scope);

            Code = "(function(){" + Code + "}).call(__jscontext);";
            source = JScriptRuntimeHolder.jsEng.CreateScriptSourceFromString(Code, SourceCodeKind.Statements);
            compiledCode = source.Compile();
        }

        // Custom javascript backwards compat args
        public class CustomCollisionArgs
        {
            public Actor OtherActor;
            public CollisionDirectionArg Direction;

            public CustomCollisionArgs(CollisionEventArgs args)
            {
                OtherActor = args.Actor2;
                Direction = args.Actor1Direction;
            }
        }

        public class CustomKeyEventArgs
        {
            public System.Windows.Input.KeyEventArgs ActualKeyEventArgs { get; private set; }
            public bool Handled { get { return ActualKeyEventArgs.Handled; } set { ActualKeyEventArgs.Handled = value; } }
            public int Key { get { return (int)ActualKeyEventArgs.Key; } }
            public int PlatformKeyCode { get { return ActualKeyEventArgs.PlatformKeyCode; } }
            public object OriginalSource { get { return ActualKeyEventArgs.OriginalSource; } }

            public override string ToString()
            {
                return ActualKeyEventArgs.ToString();
            }
            
            public CustomKeyEventArgs(PGCKeyEventArgs args)
            {
                ActualKeyEventArgs = args.keyEventArgs;
            }
        }

        public Object GenerateCustomCodeEventArgs(PGCEventArgs args)
        {
            if (args == null)
            {
                return args;
            }
            else if (args.GetType() == typeof(PGCKeyEventArgs))
            {
                return new CustomKeyEventArgs((PGCKeyEventArgs)args);
            }
            else if (args.GetType() == typeof(CollisionEventArgs))
            {
                return new CustomCollisionArgs((CollisionEventArgs)args);
            }
            else
            {
                return args;
            }
        }

        public void Invoke(BehaviorOwner sender, PGCEventArgs args)
        {
            try
            {
                scope.SetVariable("sender", sender);
                scope.SetVariable("eventArgs", GenerateCustomCodeEventArgs(args));
                scope.SetVariable("__jscontext", owner);

                compiledCode.Execute(scope);
            }
            catch (Exception e)
            {
                Console.ReportError("Error: Exception executing custom code\n" + 
                    "Message: '" + e.Message + "'\n" + 
                    "Exception: " + e.ToString() + "\n" +
                    "Code: \n" + Code + "\n\n");
            }
        }
    }
}
